﻿//Copyright (c) 2011 Shane Baker
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:

//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

using System.IO;

namespace XRGLib
{
    /// <summary>
    /// The type of layout of the font
    /// </summary>
    public enum FontLayout
    {
        /// <summary>
        /// Increasing codes from left to right
        /// </summary>
        InRow,

        /// <summary>
        /// Increasing codes from top to bottom
        /// </summary>
        InColumn,

        /// <summary>
        /// Special layout used by Libtcod
        /// </summary>
        TCOD
    }

    /// <summary>
    /// Determines how the characters will be blended when drawn on the screen
    /// </summary>
    public enum FontType
    {
        /// <summary>
        /// A key color is determined by the SPACE character.  All pixels with that color
        /// are set as fully transparent, all other pixels fully opaque
        /// </summary>
        NoAA,

        /// <summary>
        /// Transparency determined by the alpha channel of the image
        /// </summary>
        AlphaAA,

        /// <summary>
        /// Transparency is determined by the brightness of the pixels (black is fully transparent, while fully opaque)
        /// </summary>
        GreyscaleAA
    }

    /// <summary>
    /// Special characters used by the default fonts
    /// </summary>
    public enum SpecialChar : short
    {
        HorizontalLine = 196,
        VerticalLine = 179,
        NorthEastLine = 191,
        NorthWestLine = 218,
        SouthEastLine = 217,
        SouthWestLine = 192,

        DoubleHorzLine = 205,
        DoubleVertLine = 186,
        DoubleNorthEast = 187,
        DoubleNorthWest = 201,
        DoubleSouthEast = 188,
        DoubleSouthWest = 200,

        TeeWest = 180,
        TeeEast = 195,
        TeeNorth = 193,
        TeeSouth = 194,

        DoubleTeeWest = 185,
        DoubleTeeEast = 204,
        DoubleTeeNorth = 202,
        DoubleTeeSouth = 203,

        CrossLines = 197,
        DoubleCrossLines = 206,

        Block1 = 176,
        Block2 = 177,
        Block3 = 178,

        ArrowNorth = 24,
        ArrowSouth = 25,
        ArrowEast = 26,
        ArrowWest = 27,

        ArrowNorthNoTail = 30,
        ArrowSouthNoTail = 31,
        ArrowEastNoTail = 16,
        ArrowWestNoTail = 17,

        DoubleArrowHorz = 29,
        DoubleArrowVert = 18,

        CheckBoxUnset = 224,
        CheckBoxSet = 225,
        RadioUnset = 9,
        RadioSet = 10,

        SubpixelNorthWest = 226,
        SubpixelNorthEast = 227,
        SubpixelNorth = 228,
        SubpixelSouthEast = 229,
        SubpixelDiagonal = 230,
        SubpixelEast = 231,
        SubpixelSouthWest = 232,

        Smilie = 1,
        SmilieInverse = 2,
        Heart = 3,
        Diamond = 4,
        Club = 5,
        Spade = 6,
        Bullet = 7,
        BulletInverse = 8,
        Male = 11,
        Female = 12,
        Note = 13,
        NoteDouble = 14,
        Light = 15,
        ExclamationDouble = 19,
        Pilcrow = 20,
        Section = 21,
        Pound = 156,
        Multiplication = 158,
        Function = 159,
        Reserved = 169,
        Half = 171,
        OneQuarter = 172,
        Copyright = 184,
        Cent = 189,
        Yen = 190,
        Currency = 207,
        ThreeQuarters = 243,
        Division = 246,
        Umlaut = 249,
        Power1 = 251,
        Power3 = 252,
        Power2 = 253,
        BulletSquare = 254

    }


    /// <summary>
    /// Represents a font used by a Console object to display characters.  Conceptually, Font is a
    /// spritesheet with functionalty to map char values to each sprite.
    /// </summary>
    public class Font
    {

        #region Properties

        /// <summary>
        /// The width of each character in pixels
        /// </summary>
        public int CharacterWidth { get; private set; }

        /// <summary>
        /// The height of each character in pixels
        /// </summary>
        public int CharacterHeight { get; private set; }

        #endregion

        #region Factory Methods

        /// <summary>
        /// Create a Font instance using the default "terminal.png" font.  This
        /// will fail and throw an exception if the "terminal.png" file is not in the current path
        /// </summary>
        /// <param name="device"></param>
        /// <returns>The created Font instance</returns>
        public static Font CreateDefaultFont(GraphicsDevice device)
        {
            if (device == null)
                throw new ArgumentNullException("device");

            return CreateFromFile(device, "terminal.png", FontLayout.InColumn, FontType.NoAA);
        }

        /// <summary>
        /// Creates a font from a Texture2D.  The character width and height are determined from the texture size and
        /// FontLayout
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="layout"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static Font CreateFromTexture(Texture2D texture, FontLayout layout, FontType type)
        {
            int w, h;
            switch (layout)
            {
                case FontLayout.InColumn:
                case FontLayout.InRow:
                    w = 16;
                    h = 16;
                    break;

                default:
                case FontLayout.TCOD:
                    w = 32;
                    h = 8;
                    break;
            }
            return CreateFromTexture(texture, layout, type, w, h);

        }

        /// <summary>
        /// Creates a font from a Texture2D.  The character width and height are determined from the texture size and
        /// specified number of rows/columns.
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="layout"></param>
        /// <param name="type"></param>
        /// <param name="numberCols"></param>
        /// <param name="numberRows"></param>
        /// <returns></returns>
        public static Font CreateFromTexture(Texture2D texture, FontLayout layout, FontType type, int numberCols, int numberRows)
        {
            if (texture == null)
                throw new ArgumentNullException("texture");
            if (numberCols < 1)
                throw new ArgumentOutOfRangeException("numberCols");
            if (numberRows < 1)
                throw new ArgumentOutOfRangeException("numberRows");

            Font font = new Font();

            font.numCols = numberCols;
            font.numRows = numberRows;
            font.CharacterWidth = texture.Width / numberCols;
            font.CharacterHeight = texture.Height / numberRows;

            font.texture = texture;

            ProcessFont(font, layout, type);

            return font;
        }

        /// <summary>
        /// Create a Font from an image file.  The number of columns and rows are determined by the FontLayout.
        /// The character width and height are determined from the texture size and
        /// specified number of rows/columns.
        /// </summary>
        /// <param name="device"></param>
        /// <param name="fileName"></param>
        /// <param name="layout"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static Font CreateFromFile(GraphicsDevice device, string fileName, FontLayout layout, FontType type)
        {
            int w, h;
            switch (layout)
            {
                case FontLayout.InColumn:
                case FontLayout.InRow:
                    w = 16;
                    h = 16;
                    break;

                default:
                case FontLayout.TCOD:
                    w = 32;
                    h = 8;
                    break;
            }
            return CreateFromFile(device, fileName, layout, type, w, h);
        }

        /// <summary>
        /// Create a Font from an image file.  The character width and height are determined from the texture size and
        /// specified number of rows/columns.
        /// </summary>
        /// <param name="device"></param>
        /// <param name="fileName"></param>
        /// <param name="layout"></param>
        /// <param name="type"></param>
        /// <param name="numberCols"></param>
        /// <param name="numberRows"></param>
        /// <returns></returns>
        public static Font CreateFromFile(GraphicsDevice device, string fileName,
             FontLayout layout, FontType type, int numberCols, int numberRows)
        {
            if (device == null)
                throw new ArgumentNullException("device");
            if (string.IsNullOrEmpty(fileName))
                throw new ArgumentNullException("fileName");
            if (numberCols < 1)
                throw new ArgumentOutOfRangeException("numberCols");
            if (numberRows < 1)
                throw new ArgumentOutOfRangeException("numberRows");
            if (!File.Exists(fileName))
                throw new ArgumentException("Font file not found", "fileName");

            Texture2D texture;

            using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                texture = Texture2D.FromStream(device, file);
            }

            return CreateFromTexture(texture, layout, type, numberCols, numberRows);
        }

        #endregion

        #region Mapping Mehtods

        /// <summary>
        /// Set a custom mapping for a single ascii code.
        /// </summary>
        /// <param name="asciiCode"></param>
        /// <param name="fontCharX">The X coordinate of the character sprite in characters (not pixels)</param>
        /// <param name="fontCharY">The Y coordinate of the character sprite in characters (not pixels)</param>
        public void MapAsciiCode(char asciiCode, int fontCharX, int fontCharY)
        {
            if (fontCharX < 0 || fontCharX >= numCols)
                throw new ArgumentOutOfRangeException("fontCharX");
            if (fontCharY < 0 || fontCharY >= numRows)
                throw new ArgumentOutOfRangeException("fontCharY");

            charMap[asciiCode].X = fontCharX;
            charMap[asciiCode].Y = fontCharY;
        }

        /// <summary>
        /// Set a series of consecutive mappings.
        /// </summary>
        /// <param name="firstAsciiCode">The starting ascii code to map</param>
        /// <param name="numberOfCodes">Number of consecutive codes to map</param>
        /// <param name="startFontCharX">The starting X coordinate of the sprite in characters (not pixels)</param>
        /// <param name="startFontCharY">The starting Y coordinate of the sprite in characters (not pixels)</param>
        /// <param name="inColumn">True if sprites are layed out in column-first order, false if row-first</param>
        public void MapConsecutiveAsciiCodes(char firstAsciiCode, int numberOfCodes, int startFontCharX, int startFontCharY, bool inColumn)
        {
            if (startFontCharX < 0 || startFontCharX >= numCols)
                throw new ArgumentOutOfRangeException("fontCharX");
            if (startFontCharY < 0 || startFontCharY >= numRows)
                throw new ArgumentOutOfRangeException("fontCharY");

            int x = startFontCharX;
            int y = startFontCharY;

            for (int i = 0; i < numberOfCodes; i++)
            {
                MapAsciiCode((char)(firstAsciiCode + i), x, y);

                if (inColumn)
                {
                    y++;
                    if (y >= numRows)
                    {
                        y = 0;
                        x++;
                        if (x >= numCols)
                            return;
                    }
                }
                else
                {
                    x++;
                    if (x >= numCols)
                    {
                        x = 0;
                        y++;
                        if (y >= numRows)
                            return;
                    }
                }
            }


        }

        /// <summary>
        /// Maps ascii codes in provided string to consecutive character sprites.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="startFontCharX">The starting X coordinate of the sprite in characters (not pixels)</param>
        /// <param name="startFontCharY">The starting Y coordinate of the sprite in characters (not pixels)</param>
        /// <param name="inColumn">True if sprites are layed out in column-first order, false if row-first</param>
        public void MapString(string str, int startFontCharX, int startFontCharY, bool inColumn)
        {
            if (startFontCharX < 0 || startFontCharX >= numCols)
                throw new ArgumentOutOfRangeException("fontCharX");
            if (startFontCharY < 0 || startFontCharY >= numRows)
                throw new ArgumentOutOfRangeException("fontCharY");

            int x = startFontCharX;
            int y = startFontCharY;

            for (int i = 0; i < str.Length; i++)
            {
                MapAsciiCode(str[i], x, y);

                if (inColumn)
                {
                    y++;
                    if (y >= numRows)
                    {
                        y = 0;
                        x++;
                        if (x >= numCols)
                            return;
                    }
                }
                else
                {
                    x++;
                    if (x >= numCols)
                    {
                        x = 0;
                        y++;
                        if (y >= numRows)
                            return;
                    }
                }
            }
        }

        #endregion

        #region Private

        internal Texture2D texture;

        Point[] charMap;

        int numRows;
        int numCols;

        internal const char SolidChar = (char)255;

        private Font()
        {
            charMap = new Point[256];
        }

        internal void SetFontSourceRect(ref Rectangle src, char c)
        {
            src.Width = CharacterWidth;
            src.Height = CharacterHeight;
            src.X = CharacterWidth * charMap[c].X;
            src.Y = CharacterHeight * charMap[c].Y;
        }

        private static void ProcessFont(Font font, FontLayout layout, FontType type)
        {
            SetMapping(font, layout);

            switch (type)
            {
                case FontType.NoAA:
                    ProcessStandard(font, layout);
                    break;

                case FontType.AlphaAA:
                    ProcessAlplhaAA(font, layout);
                    break;

                default:
                case FontType.GreyscaleAA:
                    ProcessGreyscaleAA(font, layout);
                    break;
            }

            MakeSolidBlock(font);
        }

        private static void SetMapping(Font font, FontLayout layout)
        {
            switch (layout)
            {
                case FontLayout.InColumn:
                    font.MapConsecutiveAsciiCodes((char)0, 256, 0, 0, true);
                    break;

                case FontLayout.InRow:
                    font.MapConsecutiveAsciiCodes((char)0, 256, 0, 0, false);
                    break;

                case FontLayout.TCOD:
                    MapTCOD(font);
                    break;
            }
        }

        private static void MapTCOD(Font font)
        {
            font.MapAsciiCode(' ', 0, 0);
            font.MapString("!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`{|}~", 1, 0, false);
            font.MapAsciiCode((char)SpecialChar.Block1, 11, 1);
            font.MapAsciiCode((char)SpecialChar.Block2, 12, 1);
            font.MapAsciiCode((char)SpecialChar.Block3, 13, 1);
            font.MapAsciiCode((char)SpecialChar.VerticalLine, 14, 1);
            font.MapAsciiCode((char)SpecialChar.HorizontalLine, 15, 1);
            font.MapAsciiCode((char)SpecialChar.CrossLines, 16, 1);
            font.MapAsciiCode((char)SpecialChar.TeeWest, 17, 1);
            font.MapAsciiCode((char)SpecialChar.TeeNorth, 18, 1);
            font.MapAsciiCode((char)SpecialChar.TeeEast, 19, 1);
            font.MapAsciiCode((char)SpecialChar.TeeSouth, 20, 1);
            font.MapAsciiCode((char)SpecialChar.SouthWestLine, 21, 1);
            font.MapAsciiCode((char)SpecialChar.NorthWestLine, 22, 1);
            font.MapAsciiCode((char)SpecialChar.NorthEastLine, 23, 1);
            font.MapAsciiCode((char)SpecialChar.SouthEastLine, 24, 1);
            font.MapAsciiCode((char)SpecialChar.SubpixelNorthWest, 25, 1);
            font.MapAsciiCode((char)SpecialChar.SubpixelNorthEast, 26, 1);
            font.MapAsciiCode((char)SpecialChar.SubpixelNorth, 27, 1);
            font.MapAsciiCode((char)SpecialChar.SubpixelSouthEast, 28, 1);
            font.MapAsciiCode((char)SpecialChar.SubpixelDiagonal, 29, 1);
            font.MapAsciiCode((char)SpecialChar.SubpixelEast, 30, 1);
            font.MapAsciiCode((char)SpecialChar.SubpixelSouthWest, 31, 1);
            font.MapAsciiCode((char)SpecialChar.ArrowNorth, 0, 2);
            font.MapAsciiCode((char)SpecialChar.ArrowSouth, 1, 2);
            font.MapAsciiCode((char)SpecialChar.ArrowWest, 2, 2);
            font.MapAsciiCode((char)SpecialChar.ArrowEast, 3, 2);
            font.MapAsciiCode((char)SpecialChar.ArrowNorthNoTail, 4, 2);
            font.MapAsciiCode((char)SpecialChar.ArrowSouthNoTail, 5, 2);
            font.MapAsciiCode((char)SpecialChar.ArrowWestNoTail, 6, 2);
            font.MapAsciiCode((char)SpecialChar.ArrowEastNoTail, 7, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleArrowVert, 8, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleArrowHorz, 9, 2);
            font.MapAsciiCode((char)SpecialChar.CheckBoxUnset, 10, 2);
            font.MapAsciiCode((char)SpecialChar.CheckBoxSet, 11, 2);
            font.MapAsciiCode((char)SpecialChar.RadioUnset, 12, 2);
            font.MapAsciiCode((char)SpecialChar.RadioSet, 13, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleVertLine, 14, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleHorzLine, 15, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleCrossLines, 16, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleTeeWest, 17, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleTeeNorth, 18, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleTeeEast, 19, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleTeeSouth, 20, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleSouthWest, 21, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleNorthWest, 22, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleNorthEast, 23, 2);
            font.MapAsciiCode((char)SpecialChar.DoubleSouthEast, 24, 2);
            font.MapString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, 3, false);
            font.MapString("abcdefghijklmnopqrstuvwxyz", 0, 4, false);
            font.MapConsecutiveAsciiCodes((char)128, 32, 0, 5, false);
            font.MapAsciiCode((char)255, font.numCols - 1, font.numRows - 1);
        }

        private static void ProcessStandard(Font font, FontLayout layout)
        {
            Color[] fontData = new Color[font.texture.Width * font.texture.Height];

            font.texture.GetData<Color>(fontData);

            // Get key color from space character
            Point c = font.charMap[' '];
            Color key = fontData[c.X * font.CharacterWidth + (c.Y * font.CharacterHeight) * font.texture.Width];

            // Set all keycolor pixels to 0 alpha
            for (int i = 0; i < fontData.Length; i++)
            {
                if (fontData[i].R == key.R && fontData[i].G == key.G && fontData[i].B == key.B)
                {
                    fontData[i].A = 0;
                    fontData[i].R = 0;
                    fontData[i].G = 0;
                    fontData[i].B = 0;
                }
                else
                {
                    fontData[i].A = 255;
                    fontData[i].R = 255;
                    fontData[i].G = 255;
                    fontData[i].B = 255;
                }
            }

            font.texture.SetData<Color>(fontData);
        }

        private static void ProcessAlplhaAA(Font font, FontLayout layout)
        {
            Color[] fontData = new Color[font.texture.Width * font.texture.Height];

            font.texture.GetData<Color>(fontData);

            // Set color according to alpha
            for (int i = 0; i < fontData.Length; i++)
            {
                fontData[i].R = 255;
                fontData[i].G = 255;
                fontData[i].B = 255;
            }

            font.texture.SetData<Color>(fontData);
        }

        private static void ProcessGreyscaleAA(Font font, FontLayout layout)
        {
            Color[] fontData = new Color[font.texture.Width * font.texture.Height];

            font.texture.GetData<Color>(fontData);

            // Set alpha according to greyscale value
            for (int i = 0; i < fontData.Length; i++)
            {
                int gs = (fontData[i].R + fontData[i].G + fontData[i].B) / 3;
                fontData[i].A = (byte)gs;
            }

            font.texture.SetData<Color>(fontData);
        }

        private static void MakeSolidBlock(Font font)
        {
            Color[] fontData = new Color[font.texture.Width * font.texture.Height];

            font.texture.GetData<Color>(fontData);

            // Create solid block at char 255 (last character)

            int sx = font.texture.Width - font.CharacterWidth;
            int sy = font.texture.Height - font.CharacterHeight;

            for (int y = 0; y < font.CharacterHeight; y++)
            {
                for (int x = 0; x < font.CharacterWidth; x++)
                {
                    fontData[x + sx + (y + sy) * font.texture.Width] = new Color(255, 255, 255, 255);
                }
            }

            font.texture.SetData<Color>(fontData);
        }

        #endregion

    }
}
